﻿'Ferruh Mavituna - http://ferruh.mavituna.com
'LGPL, see installer/licence.txt

Imports System.IO
Imports System.Threading
Imports System.Xml.Serialization

''' <summary>
''' Psycho Folder, Triggers rules based on the files in a folder.
''' </summary>
Public Class FrmMain
#Region "Internal Settings"
	Private Const FILE_ACCESS_RETRY_SLEEP As Integer = 5000
	Private Const FILE_ACCESS_RETRY_COUNT As Integer = 50

	Private Const FILE_HOLDER As String = "{FILE}"
	Private Const PATH_HOLDER As String = "{PATH}"

#End Region

	Private KillList As New List(Of RunningApp)(5)
	Private Watcher As FileSystemWatcher

	''' <summary>
	''' Rules
	''' </summary>
	Private CurrentRules As RuleManager

	''' <summary>
	''' Shows a simple popup with the error message.
	''' </summary>
	''' <param name="title">The title.</param>
	''' <param name="message">The message.</param>
	Private Sub ReportError(ByVal title As String, ByVal message As String)
		NtfIcon.ShowBalloonTip(2000, title, message, ToolTipIcon.Error)
	End Sub

	''' <summary>
	''' Checks the folder path and pops up select folder dialog.
	''' </summary>
	Private Function CheckPath() As Boolean
		If String.IsNullOrEmpty(My.Settings.Path) OrElse Not IO.Directory.Exists(My.Settings.Path) Then

			If MessageBox.Show("You need to choose a folder to watch. When you click OK to this message you can choose a new folder to watch.", "Please choose a directory to watch", MessageBoxButtons.OKCancel, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1) = Windows.Forms.DialogResult.OK Then

				'ReportError("Watch directory error", String.Format("{0} does not exist. Set a correct path from the Rules.xml and restart again.", My.Settings.Path))
				SelectFolder()

			End If
		End If

		Return Not String.IsNullOrEmpty(My.Settings.Path) AndAlso IO.Directory.Exists(My.Settings.Path)

	End Function


	''' <summary>
	''' Checks the folder path and pops up select folder dialog.
	''' </summary>
	Private Function CheckRules() As Boolean
		If String.IsNullOrEmpty(My.Settings.RulesFile) Then

			If MessageBox.Show("You need to choose a folder to watch. When you click OK to this message you can choose a new folder to watch.", "Please choose a directory to watch", MessageBoxButtons.OKCancel, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1) = Windows.Forms.DialogResult.OK Then

				ReportError("Watch directory error", String.Format("{0} does not exist. Set a correct path from the Rules.xml and restart again.", My.Settings.Path))
				SelectFolder()

			End If
		End If

		Return Not String.IsNullOrEmpty(My.Settings.Path) AndAlso IO.Directory.Exists(My.Settings.Path)

	End Function


	''' <summary>
	''' Prepares, Checks etc. Startup stuff
	''' </summary>
	Private Sub Init()
		'Restore from settings (somehoww binding doesn't work)
		ToolExecSimul.Checked = My.Settings.CheckSimul

		'Load Rules
		Try
			CurrentRules = RuleManager.LoadRules(My.Settings.RulesFile)

		Catch ex As Exception
			ReportError("Rules Load Error", "Couldn't load the Rules. Right click to this icon and select ""Select a Rules File"" to choose a different Rules file.")
			Exit Sub

		End Try

		'Check if there is rule to process
		If CurrentRules Is Nothing OrElse CurrentRules.Rules.Count = 0 Then
			ReportError("There is no rule", "There is no rule to process. Ensure that your rules files is in there, correctly syntaxed and accessible. Right click to this icon and select ""Select a Rules File"" to choose a different Rules file.")
			Exit Sub
		End If

		'Visibility etc.
		Me.Text = String.Format(Me.Text, My.Application.Info.Version.ToString())
		NtfIcon.Text = Me.Text
		Me.WindowState = FormWindowState.Minimized
		Me.ShowInTaskbar = False

		'Start
		If CheckPath() Then
			StartWatcher()

		Else
			ReportError("Choose a folder to watch.", "You need to choose a folder to watch, you can do this by double clicking to the icon")

		End If

	End Sub


	''' <summary>
	''' Start watcher
	''' </summary>
	Private Sub StartWatcher()
		If Not CheckPath() Then Exit Sub

		Watcher = New FileSystemWatcher()

		With Watcher
			.Path = My.Settings.Path
			.IncludeSubdirectories = Me.ToolWatchSubfolders.Checked

			AddHandler .Created, AddressOf FileCreated
			AddHandler .Renamed, AddressOf FileCreated
			.EnableRaisingEvents = True

			If CurrentRules IsNot Nothing Then
				NtfIcon.ShowBalloonTip(2000, String.Format("Started to watch {0}", .Path), String.Format("Watching for new files. {0} rules loaded.", CurrentRules.Rules.Count), ToolTipIcon.Info)
			End If

		End With
	End Sub



	''' <summary>
	''' Handles newly Created/Renamed files in the watch folder. 
	''' </summary>
	''' <param name="sender">The sender.</param>
	''' <param name="e">The <see cref="System.IO.FileSystemEventArgs" /> instance containing the event data.</param>
	Private Sub FileCreated(ByVal sender As Object, ByVal e As FileSystemEventArgs)

		For Each rule As Rule In CurrentRules.Rules
			If rule.Extension.Equals(Path.GetExtension(e.FullPath), StringComparison.OrdinalIgnoreCase) Then
				NtfIcon.ShowBalloonTip(1000, "Executing Rule", String.Format("Executing the {0} for {1}", rule.Name, e.Name), ToolTipIcon.Info)

				Dim executeSettings As ExecuteData = New ExecuteData(rule, e.FullPath, Path.GetDirectoryName(e.FullPath), Not ToolExecSimul.Checked)

				'Run in a separate thread, don't block anything
				Dim thr As New Thread(AddressOf LockExecuter)
				thr.Start(executeSettings)
				
				'Block before processing the next rule. This can be important if the rule uses SendKey
				Thread.Sleep(rule.RuleWait)
			End If
		Next

	End Sub

	Private _ExecLock As New Object()


	Private Sub LockExecuter(ByVal data As Object)
		Dim executeData = DirectCast(data, ExecuteData)

		If executeData.Wait Then
			SyncLock _ExecLock
				Execute(executeData)
			End SyncLock

		Else
			Execute(executeData)

		End If

	End Sub

	''' <summary>
	''' Executes the specified rule.
	''' </summary>
	''' <param name="data">The execute data.</param>
	Private Sub Execute(data As ExecuteData)

		Dim command = data.Rule.Run

		command = command.Replace(FILE_HOLDER, data.FullPath)
		command = command.Replace(PATH_HOLDER, data.Path)

		Dim RetryCount As Integer = FILE_ACCESS_RETRY_COUNT

		While (True)
			If RetryCount = 0 Then
				NtfIcon.ShowBalloonTip(1000, "Read Error", String.Format("File still in use : {0}.", data.FullPath), ToolTipIcon.Error)
				Exit While
			End If

			If FileReady(data.FullPath) Then

				'"cmd.exe /c " & 
				Dim PID As Integer
				Try
					PID = Shell(command, AppWinStyle.MinimizedNoFocus, data.Wait)

				Catch ex As IOException

					ReportError("There was an error with the command.", String.Format("The command just triggered caused an error. The command was ""{0}""{1}{1}{2}", command, Environment.NewLine, ex.ToString))
					Exit Sub

				End Try

				If Not String.IsNullOrEmpty(data.Rule.SendKeys) AndAlso PID > 0 Then
					Thread.Sleep(data.Rule.KeyWait)

					AppActivate(PID)

					Thread.Sleep(data.Rule.KeyWait)

					KeyProcessor.Process(data.Rule.SendKeys)

				End If

				If Not data.Wait Then KillList.Add(New RunningApp(PID))

				'Succeeded
				Exit While
			End If

			'Wait a second and retry
			RetryCount -= 1
			System.Threading.Thread.Sleep(FILE_ACCESS_RETRY_SLEEP)
		End While

	End Sub

	''' <summary>
	''' Is File ready to read?
	''' </summary>
	''' <param name="filePath">The file path.</param>
	''' <returns></returns>
	Private Shared Function FileReady(ByVal filePath As String) As Boolean
		Try
			Using testStream As FileStream = File.Open(filePath, FileMode.Open, FileAccess.Read)
				Return True
			End Using
		Catch ex As IOException
			Return False
		End Try
	End Function

	''' <summary>
	''' Cleans up ProcessIds which marked for kill
	''' </summary>
	Private Sub CleanUp()
		If KillList.Count = 0 Then Exit Sub

		For Each app As RunningApp In Me.KillList.ToArray()
			If DateTime.Now.Subtract(app.StartTime).TotalSeconds > 200 Then
				KillList.Remove(app)
				app.Kill()
			End If
		Next

	End Sub



	''' <summary>
	''' Shows the dialog and load a new Rule File
	''' </summary>
	Private Sub ChangeRuleFile()
		If DlgRule.ShowDialog = Windows.Forms.DialogResult.OK Then
			My.Settings.RulesFile = DlgRule.FileName
			My.Settings.Save()
		End If

		StopWatcher()

		'Load new rules
		CurrentRules = RuleManager.LoadRules(My.Settings.RulesFile)

		'Start Watcher
		StartWatcher()
	End Sub


	''' <summary>
	''' Selects the folder and start watching.
	''' </summary>
	Private Sub SelectFolderAndStartWatching()
		If SelectFolder() Then
			StopWatcher()
			StartWatcher()
		End If
	End Sub

	''' <summary>
	''' Selects a new folder to watch.
	''' </summary>
	Private Function SelectFolder() As Boolean
		If DlgFolder.ShowDialog = Windows.Forms.DialogResult.OK Then
			My.Settings.Path = DlgFolder.SelectedPath
			My.Settings.Save()
			Return True

		Else
			Return False

		End If
	End Function


	''' <summary>
	''' Stops the folder watcher.
	''' </summary>
	Private Sub StopWatcher()
		'Stop Watcher
		If Watcher IsNot Nothing Then
			Watcher.EnableRaisingEvents = False
			RemoveHandler Watcher.Created, AddressOf FileCreated
			RemoveHandler Watcher.Renamed, AddressOf FileCreated
			Watcher = Nothing
		End If
	End Sub



#Region "GUI Interaction"

	Private Sub FrmMain_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
		Init()
	End Sub

	Private Sub ExitToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ExitToolStripMenuItem.Click
		Me.Close()
	End Sub

	Private Sub SelectAFolderToWatchToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles SelectAFolderToWatchToolStripMenuItem.Click
		SelectFolderAndStartWatching()
	End Sub

	Private Sub NtfIcon_MouseDoubleClick(ByVal sender As System.Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles NtfIcon.MouseDoubleClick
		DefaultIconAction()
	End Sub

	Private Sub DefaultIconAction()
		If Watcher Is Nothing Then
			SelectFolderAndStartWatching()

		Else
			Process.Start(Watcher.Path)

		End If
	End Sub

	Private Sub SelectRulesFileToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles SelectRulesFileToolStripMenuItem.Click
		ChangeRuleFile()
	End Sub

	Private Sub HelpToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles HelpToolStripMenuItem.Click
		Try
			Process.Start("http://code.google.com/p/psychofolder/")
		Catch ex As Exception

		End Try

	End Sub

	Private Sub AboutToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles AboutToolStripMenuItem.Click
		NtfIcon.ShowBalloonTip(2000, Me.Text, "Ferruh Mavituna - http://ferruh.mavituna.com", ToolTipIcon.Info)
	End Sub


	Private Sub TmrKiller_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TmrKiller.Tick
		CleanUp()
	End Sub


	Private Sub MnuBrowseFolder_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MnuBrowseFolder.Click
		DefaultIconAction()
	End Sub

	Private Sub CheckState(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ToolWatchSubfolders.CheckStateChanged
		RestartWatched()
	End Sub

#End Region

#Region "Structures"
	''' <summary>
	''' Thread starter
	''' </summary>
	''' <remarks></remarks>
	Private Structure ExecuteData
		Public Rule As Rule
		Public FullPath, Path As String
		Public Wait As Boolean

		''' <summary>
		''' Initializes a new instance of the ExecuteData structure.
		''' </summary>
		''' <param name="rule"></param>
		''' <param name="fullPath"></param>
		''' <param name="path"></param>
		Public Sub New(ByVal rule As Rule, ByVal fullPath As String, ByVal path As String, wait As Boolean)
			Me.Rule = rule
			Me.FullPath = fullPath
			Me.Path = path
			Me.Wait = wait
		End Sub
	End Structure

#End Region


	Private Sub RestartWatched()
		Me.StopWatcher()
		Me.StartWatcher()
	End Sub

	Private Sub ToolExecSimul_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ToolExecSimul.Click
		'Not sure why binding doesn't work for MeunItems.check/state
		My.Settings.CheckSimul = ToolExecSimul.Checked
	End Sub
End Class
